package tv.dyndns.kishibe.client;

import java.util.logging.Level;
import java.util.logging.Logger;

import net.zschech.gwt.websockets.client.MessageEvent;
import net.zschech.gwt.websockets.client.MessageHandler;
import net.zschech.gwt.websockets.client.WebSocket;
import tv.dyndns.kishibe.client.constant.Constant;
import tv.dyndns.kishibe.client.packet.PacketUserData.WebSocketUsage;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * ポーリングを行うための補助クラス。タイマー+RPCによるポーリングとWebSocketによるポーリングエミュレーションの両方に対応している。
 * 
 * @author nodchip
 * @param <T>
 */
public abstract class StatusUpdater<T> {
	private static final Logger logger = Logger.getLogger(StatusUpdater.class.getName());
	private static final int ENABLED_USER_CODE_LOWER_DIGIT = 10;
	private static final int MAX_WEBSOCKET_FAILED_COUNT = 2;
	private static final int MAX_RESPONSE_RECIEVE_FAILED_COUNT = 5;
	private final String path;
	private final int intervalMs;
	private boolean running;
	private WebSocket webSocket;
	private int webSocketFailedCount = 0;
	private int responseRecieveFailedCount = 0;
	@VisibleForTesting
	final RepeatingCommand commandUpdate = new RepeatingCommand() {
		@Override
		public boolean execute() {
			try {
				request(callback);
			} catch (Exception e) {
				logger.log(Level.WARNING, "リクエスト中にエラーが発生しました", e);
			}
			return running;
		}
	};
	@VisibleForTesting
	final AsyncCallback<T> callback = new AsyncCallback<T>() {
		@Override
		public void onSuccess(T result) {
			try {
				onReceived(result);
			} catch (Exception e) {
				logger.log(Level.WARNING, "レスポンス処理中にエラーが発生しました(RPC)", e);
			}
		};

		@Override
		public void onFailure(Throwable caught) {
			if (++responseRecieveFailedCount < MAX_RESPONSE_RECIEVE_FAILED_COUNT) {
				logger.log(Level.WARNING, "レスポンス取得中にエラーが発生しました", caught);
			} else {
				logger.log(Level.SEVERE,
						"レスポンス取得中にエラーが発生しました。致命的なエラーを避けるため通信を終了します。ページをリロードして下さい。", caught);

				stop();
			}
		}
	};

	public StatusUpdater(String path, int intervalMs) {
		this.path = Preconditions.checkNotNull(path);
		this.intervalMs = intervalMs;
	}

	public void start() {
		if (doesUseWebSocket()) {
			try {
				webSocket = WebSocket.create(Constant.WEB_SOCKET_URL + path);
			} catch (JavaScriptException e) {
				// WebSocket is not supported.
			}
		}

		if (webSocket == null) {
			Scheduler.get().scheduleFixedDelay(commandUpdate, intervalMs);
		} else {
			webSocket.setOnMessage(messageHandler);
		}

		running = true;
	}

	@VisibleForTesting
	boolean doesUseWebSocket() {
		WebSocketUsage webSocketUsage = UserData.get().getWebSocketUsage();
		return webSocketUsage == WebSocketUsage.On || webSocketUsage == WebSocketUsage.Default
				&& UserData.get().getUserCode() % 10 <= ENABLED_USER_CODE_LOWER_DIGIT;
	}

	private final MessageHandler messageHandler = new MessageHandler() {
		@Override
		public void onMessage(WebSocket webSocket, MessageEvent event) {
			String data = event.getData();

			// System.out.println(data);

			T status;
			try {
				status = parse(data);
			} catch (Exception e) {
				logger.log(Level.WARNING, "レスポンスのパース中にエラーが発生しました", e);

				if (++webSocketFailedCount >= MAX_WEBSOCKET_FAILED_COUNT) {
					fallback(webSocket);
				}

				return;
			}

			try {
				onReceived(status);
			} catch (Exception e) {
				logger.log(Level.WARNING, "レスポンス処理中にエラーが発生しました(WebSocket)。フォールバックします。", e);

				if (++webSocketFailedCount >= MAX_WEBSOCKET_FAILED_COUNT) {
					fallback(webSocket);
				}
			}
		}
	};

	private void fallback(WebSocket webSocket) {
		Scheduler.get().scheduleFixedDelay(commandUpdate, intervalMs);
		webSocket.close();

		logger.log(Level.WARNING, "通常のRPCにフォールバックしました");
	}

	public void stop() {
		running = false;
		if (webSocket != null) {
			webSocket.close();
			webSocket = null;
		}
	}

	/**
	 * RPCによるリクエストを行う。コールバックメソッドとして第一引数のcallbackを渡さなければならない。
	 * 
	 * @param callback
	 */
	protected abstract void request(AsyncCallback<T> callback);

	/**
	 * メッセージを取得した後の動作を行う
	 * 
	 * @param status
	 *            ステータス
	 */
	protected abstract void onReceived(T status);

	/**
	 * jsonをパースしてメッセージに変換する
	 * 
	 * @param json
	 *            　json文字列
	 * @return メッセージ
	 */
	protected abstract T parse(String json);

}